package com.jgraph.pad.persistence;

import java.beans.DefaultPersistenceDelegate;
import java.beans.Encoder;
import java.beans.ExceptionListener;
import java.beans.Expression;
import java.beans.PersistenceDelegate;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.jgraph.graph.ConnectionSet;
import org.jgraph.graph.DefaultEdge;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.DefaultGraphModel;
import org.jgraph.graph.DefaultPort;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.GraphLayoutCache;

import com.jgraph.editor.JGraphEditorFile;
import com.jgraph.editor.persistence.JGraphPersistenceManagerFactoryMethod;
import com.jgraph.pad.JGraphpadDiagram;
import com.jgraph.pad.JGraphpadLibrary;
import com.jgraph.pad.graph.JGraphpadBusinessObject;
import com.jgraph.pad.graph.JGraphpadGraphConstants;
import com.jgraph.pad.graph.JGraphpadGraphLayoutCache;
import com.jgraph.pad.graph.JGraphpadGraphModel;
import com.jgraph.pad.graph.JGraphpadRichTextValue;
import com.jgraph.pad.util.JGraphpadParallelEdgeRouter;
import com.jgraph.pad.util.JGraphpadParallelSplineRouter;
import com.jgraph.pad.util.JGraphpadShadowBorder;

/**
 * This class defines a persistence manager that handles (reads/writes)
 * {@link JGraphEditorFile}s. Such files are encoded via the standard Java
 * Bean XML persistence.
 *
 * @author svetoslavganov@mail.utexas.edu
 *
 */
public class JGraphpadBeanXMLPersistenceManager
        extends JGraphpadAbstractPersistenceManager<JGraphEditorFile> {

	/**
	 * Default Java Bean XML encoding file type.
	 */
	public static final String TYPE_JAVA_BEAN_XML = "xml";

	/**
	 * Default Java Bean XML encoding file type - compressed.
	 */
	public static final String TYPE_JAVA_BEAN_XML_COMPRESSED = "xml.gz";

	/**
	 * All supported file types by this persistence manager.
	 */
	public static final String[] SUPPORTED_FILE_TYPES = new String[] {
		    TYPE_JAVA_BEAN_XML,
		    TYPE_JAVA_BEAN_XML_COMPRESSED
	    };

	private Map<Class<?>, PersistenceDelegate> mPersistenceDelegates =
		new HashMap<Class<?>, PersistenceDelegate>();

	public JGraphpadBeanXMLPersistenceManager() {
        addDefaultPersistenceDelegates();		
	}

	/**
	 * This implementation adds persistence delegates for the following classes:
	 * {@link JGraphpadDiagram},{@link JGraphpadGraphModel},
	 * com.jgraph.graph.ConnectionSet, {@link JGraphpadGraphLayoutCache},
	 * com.jgraph.graph.DefaultGraphCell, com.jgraph.graph.DefaultEdge,
	 * com.jgraph.graph.DefaultPort,{@link JGraphpadBusinessObject},
	 * {@link JGraphpadRichTextValue}and {@link JGraphpadShadowBorder}.<br>
	 * <b>JGraphpadDiagram </b> <br>
	 * Constructs a persistence delegate for the diagram class. This uses the
	 * fact that all information is stored in the model, not the layout cache by
	 * ignoring the layout cache and accessing the model stored in the layout
	 * cache directly through the model bean property. Note: To allow this kind
	 * of encoding the diagram class offers a special constructor that takes a
	 * model and constructs a new graph layout cache for it. <br>
	 * <b>JGraphpadGraphModel </b> <br>
	 * To encode graph models we do not want the files to contain redundant
	 * connectivity information in the ports.edges and edges.source and target
	 * fields, so we add a method to the graph model that returns a connection
	 * set which describes the connections without redundancy. (Note: In the
	 * static initializer of this class we make sure that the edges, source and
	 * target of the respective classes or not encoded.) <br>
	 * <b>ConnectionSet </b> <br>
	 * The complete information of a connection set is stored in the actual
	 * connections, thus we only store the connections and use special
	 * constructor to restore the state of the complete object when de- coding.
	 * (Note: For connection sets this will update the edges field.) <br>
	 * <b>JGraphpadGraphLayoutCache </b> <br>
	 * The graph layout cache is encoded by encoding the various member fields,
	 * using a special constructor to restore the state of the layout cache upon
	 * decoding. Note that this is currently not used. <br>
	 * <b>DefaultGraphCell, DefaultEdge, DefaultPort </b> <br>
	 * Makes sure the cells are only encoded along with their user objects, the
	 * attributes, connections and tree-structure is stored in other objects and
	 * does not need to be encoded here. <br>
	 * <b>JGraphpadBusinessObject, JGraphpadRichTextData </b> <br>
	 * Allows to encode custom business objects used in JGraphpad. Since this
	 * object implements the bean interface we do only require a default
	 * persistence delegates with no special constructor calls to decode the
	 * object. Same holds for the rich text data object, which is a special
	 * value that can hold text formatting information. <br>
	 * <b>JGraphShadowBorder </b> <br>
	 * Since the shadow border is a singleton we must tell the decoder which
	 * method to use in order to find the shared instance of the class.
	 */
	protected void addDefaultPersistenceDelegates() {
		// Adds default persistence delegates
		addPersistenceDelegate(JGraphpadDiagram.class,
				new DefaultPersistenceDelegate(new String[] { "name",
						"graphLayoutCache", "properties" }));

		addPersistenceDelegate(JGraphpadLibrary.class,
				new DefaultPersistenceDelegate(new String[] { "filename",
						"graphLayoutCache" }));

		addPersistenceDelegate(JGraphpadGraphModel.class,
				new DefaultPersistenceDelegate(new String[] { "roots",
						"attributes", "connectionSet" }));

		addPersistenceDelegate(ConnectionSet.class,
				new DefaultPersistenceDelegate(new String[] { "connections" }));

		addPersistenceDelegate(JGraphpadGraphLayoutCache.class,
				new DefaultPersistenceDelegate(new String[] { "model",
						"visibleSet", "partial" }));

		addPersistenceDelegate(DefaultGraphCell.class,
				new DefaultPersistenceDelegate(new String[] { "userObject" }));

		addPersistenceDelegate(DefaultEdge.class,
				new DefaultPersistenceDelegate(new String[] { "userObject" }));

		addPersistenceDelegate(DefaultPort.class,
				new DefaultPersistenceDelegate(new String[] { "userObject" }));

		addPersistenceDelegate(JGraphpadBusinessObject.class,
				new DefaultPersistenceDelegate());

		addPersistenceDelegate(JGraphpadRichTextValue.class,
				new DefaultPersistenceDelegate());

		// Shadow Border instance
		addPersistenceDelegate(JGraphpadShadowBorder.class,
				new PersistenceDelegate() {
					protected Expression instantiate(Object oldInstance,
							Encoder out) {
						return new Expression(oldInstance,
								JGraphpadShadowBorder.class,
								"getSharedInstance", null);
					}
				});

		// Two shared routing instances
		addPersistenceDelegate(JGraphpadParallelEdgeRouter.class,
				new PersistenceDelegate() {
					protected Expression instantiate(Object oldInstance,
							Encoder out) {
						return new Expression(oldInstance,
								JGraphpadGraphConstants.class,
								"getParallelEdgeRouting", null);
					}
				});
		addPersistenceDelegate(
				JGraphpadParallelSplineRouter.class,
				new PersistenceDelegate() {
					protected Expression instantiate(Object oldInstance,
							Encoder out) {
						return new Expression(oldInstance,
								JGraphpadGraphConstants.class,
								"getParallelSplineRouting", null);
					}
				});

		addPersistenceDelegate(DefaultGraphModel.class,
				new DefaultPersistenceDelegate(new String[] { "roots",
						"attributes" }));

		addPersistenceDelegate(GraphLayoutCache.class,
				new DefaultPersistenceDelegate(new String[] { "model",
						"factory", "cellViews", "hiddenCellViews",
						"partial" }));

		// DefaultEdge.DefaultRouting has a shared instance which may
		// be retrieved using GraphConstants.getROUTING_SIMPLE().
		addPersistenceDelegate(DefaultEdge.DefaultRouting.class,
				new PersistenceDelegate() {
					protected Expression instantiate(Object oldInstance,
							Encoder out) {
						return new Expression(oldInstance,
								GraphConstants.class, "getROUTING_SIMPLE",
								null);
					}
				});

		addPersistenceDelegate(DefaultEdge.LoopRouting.class,
				new PersistenceDelegate() {
					protected Expression instantiate(Object oldInstance,
							Encoder out) {
						return new Expression(oldInstance,
								GraphConstants.class, "getROUTING_DEFAULT",
								null);
					}
				});
	}

	/**
	 * Associates the specified persistence delegate with <code>clazz</code>
	 * for XML encoding.
	 *
	 * @param clazz The class to associate the delegate with.
	 * @return Returns the previous delegate for <code>clazz</code>.
	 */
	public Object addPersistenceDelegate(Class<?> clazz,
			PersistenceDelegate delegate) {
		if (delegate != null) {
			return mPersistenceDelegates.put(clazz, delegate);
		}
		return null;
	}

	/**
	 * Returns the associated persistence delegate for <code>clazz</code> or
	 * <code>null</code> if no association exists.
	 *
	 * @param clazz The class to return the delegate for.
	 * @return Returns the persistence delegate for <code>clazz</code> or
	 *         <code>null</code>.
	 */
	public PersistenceDelegate getPersistenceDelegate(Class<?> clazz) {
		if (clazz != null) {
			return (PersistenceDelegate) mPersistenceDelegates.get(clazz);
		}
		return null;
	}

	/**
	 *Removes the associated persistence delegate for <code>clazz</code> or
	 * <code>null</code> if no association exists.
	 *
	 * @param clazz The class to return the delegate for.
	 * @return Returns the removed persistence delegate or <code>null</code>.
	 */
	public PersistenceDelegate removePersistenceDelegate(Class<?>  clazz) {
		return mPersistenceDelegates.remove(clazz);
	}

	public JGraphEditorFile readObject(String uri)
	        throws MalformedURLException, IOException {
		if (uri != null) {
	        InputStream in = getInputStream(uri);
			Object file = readObject(in);
			in.close();
			if (file instanceof JGraphEditorFile) {
			    ((JGraphEditorFile) file).setFilename(uri);
				return (JGraphEditorFile) file;
			}
		}
		return null;
	}

	public void writeObject(JGraphEditorFile file, String uri)
	        throws MalformedURLException, IOException {
		final List<Exception> problems = new LinkedList<Exception>();
		if (uri != null) {
			XMLEncoder encoder = new XMLEncoder(getOutputStream(uri));
			encoder.setExceptionListener(new ExceptionListener() {
				public void exceptionThrown(Exception e) {
					problems.add(e);
				}
			});
			configureEncoder(encoder);
			encoder.writeObject(file);
	        encoder.close();
		}
		if (!problems.isEmpty())
			throw new RuntimeException(problems.size()
					+ " errors while writing " + uri + " ("
					+ problems.get(0) + ")");
	}

	/**
	 * Hook for subclasses to configure a new XML encoder for writing an
	 * object. This implementation sets all registered persistence delegates and
	 * installs default mappings for classes (e.g. it assigns the list
	 * persistence delegates to array lists).
	 * 
	 * @param enc
	 *            The encoder to be configured.
	 */
	protected void configureEncoder(XMLEncoder encoder) {
		for (Map.Entry<Class<?>, PersistenceDelegate> e
				: mPersistenceDelegates.entrySet()) {
			encoder.setPersistenceDelegate(e.getKey(), e.getValue());
		}
		encoder.setPersistenceDelegate(ArrayList.class, encoder
				.getPersistenceDelegate(List.class));
	}

	/**
	 * Returns a new object from the specified stream using a new XML decoder.
	 *
	 * @return Returns a new object from the specified stream.
	 */
	protected Object readObject(InputStream in) {
		XMLDecoder dec = new XMLDecoder(in);
		if (dec != null) {
			Object obj = dec.readObject();
			dec.close();
			return obj;
		}
		return null;
	}

	/**
	 * Factory method for {@link JGraphpadBeanXMLPersistenceManager}s.
	 */
	public static class FactoryMethod
	        extends JGraphPersistenceManagerFactoryMethod<JGraphEditorFile> {

		public FactoryMethod(Properties properties) {
			super(SUPPORTED_FILE_TYPES, properties);
		}

		public JGraphpadAbstractPersistenceManager<JGraphEditorFile>
		        createPersistenceManager(Properties properties) {
			return new JGraphpadBeanXMLPersistenceManager();
		}
	}
}
